//
//  SearchEngine.m
//  Simple Tagger
//
//  Created by Thomas Bröckers on 01.02.08.
//  Copyright 2008 __MyCompanyName__. All rights reserved.
//

#import "SearchEngine.h"
#import "NSStringExtension.h"
#import "Track.h"
#import <CommonCrypto/CommonDigest.h>
#import <CommonCrypto/CommonHMAC.h>
#import <CommonCrypto/CommonCryptor.h>

NSInteger stringByteSort(NSString *a, NSString *b, void *context);

#define AWS_KEY_ID @"AKIAJXFXQQBQ53QFQU3Q"
#define SECRET_ACCESS_ID @"fdWy93uKrGBSBOfmyyypYHxGX3pjOSctUh+SJ9wj"
#define AWS_PATH @"/onca/soap"

@interface SearchEngine (private)
- (NSString *) replaceUnWantedCharactersFromSearchString:(NSString *) searchString;
@end

@implementation SearchEngine

- (NSMutableArray *) buildSearchResultFromKeywords:(NSString *)keywords
                                        withEngine:(NSString *)engineName {

    NSString *responseGroup, *operation, *searchIndex, *service;
    
    responseGroup = @"Large";
    operation = @"ItemSearch";
    searchIndex = @"Music";
    service = @"AWSECommerceService";
    
    NSDictionary *params = [[NSMutableDictionary alloc] init];
    [params setValue:operation forKey:@"Operation"];
    [params setValue:searchIndex forKey:@"SearchIndex"];
    [params setValue:[self replaceUnWantedCharactersFromSearchString:[keywords mutableCopy]] forKey:@"Keywords"];
    [params setValue:@"1" forKey:@"ItemPage"];
    [params setValue:responseGroup forKey:@"ResponseGroup"];
    [params setValue:service forKey:@"Service"];
    [params setValue:[self utcTimestamp] forKey:@"Timestamp"];
    [params setValue:AWS_KEY_ID forKey:@"AWSAccessKeyId"];
    
    NSString *host = @"";
    if ([engineName isEqualToString:@"de"]) {
        host = @"ecs.amazonaws.de";
    }
    else if ([engineName isEqualToString:@"com"]) {
        host = @"ecs.amazonaws.com";
    }
    else if ([engineName isEqualToString:@"co.uk"]) {
        host = @"ecs.amazonaws.co.uk";
    }
    else if ([engineName isEqualToString:@"fr"]) {
        host = @"ecs.amazonaws.fr";
    }
    
    NSString *paramsForSignature = [self queryStringForParameterDictionary:params];
    NSString *stringForSignature = [self signatureInputForQueryString:paramsForSignature AndHost:host];
    NSString *signature = [self makeSignature:SECRET_ACCESS_ID 
                                         data:[stringForSignature 
                                               dataUsingEncoding:NSASCIIStringEncoding]];
    
    NSString *urlString = [NSString stringWithFormat:@"http://%@%@?%@&Signature=%@", 
                           host, AWS_PATH, paramsForSignature, signature];
	NSError *error = nil;
	NSXMLDocument *doc = [[[NSXMLDocument alloc] initWithContentsOfURL:[NSURL URLWithString:urlString] 
                                                               options:0 
                                                                 error:&error] autorelease]; 
    
    #ifdef DEBUG
        //NSLog(@"characterEncoding: %@", [doc characterEncoding]);
        NSLog(@"urlString: %@", urlString);    
        //NSLog(@"Result: %@", doc);
    #endif
    
    /*
     Die Abfrage muss für jede Page (10 Ergebnisse pro Seite) einzeln mit dem zusätzlichen
     Parameter ItemPage ausgeführt und mit den Ergebnissen das Suchergebnis aufgebaut werden.
     Für die erste Seite ist dies nicht erforderlich, da sie bereits mit der ersten Anfrage
     geliefert wurde.
     */
    NSMutableArray *resultList = [[NSMutableArray alloc] init];
   
    // Die Ergebnisse der ersten Seiten können bereits hinzugefügt werden.
    [self addResultEntriesFromResult:doc ToResultList:resultList];
    
    // Baue jetzt die Schleife für die weiteren Seiten auf.
    int i;
    int totalPages = [self getTotalPagesFromItemSearchResult:doc];
    
    // Beschränke die maximale Ergebnisanzahl auf 3 Seiten
    if (totalPages > 3) totalPages = 3;

    for (i = 2; i <= totalPages; i++) {

        NSDictionary *params = [[NSMutableDictionary alloc] init];
        [params setValue:operation forKey:@"Operation"];
        [params setValue:searchIndex forKey:@"SearchIndex"];
        [params setValue:[self replaceUnWantedCharactersFromSearchString:[keywords mutableCopy]] forKey:@"Keywords"];
        [params setValue:[NSString stringWithFormat:@"%d", i] forKey:@"ItemPage"];
        [params setValue:responseGroup forKey:@"ResponseGroup"];
        [params setValue:service forKey:@"Service"];
        [params setValue:[self utcTimestamp] forKey:@"Timestamp"];
        [params setValue:AWS_KEY_ID forKey:@"AWSAccessKeyId"];
        
        NSString *paramsForSignature = [self queryStringForParameterDictionary:params];
        NSString *stringForSignature = [self signatureInputForQueryString:paramsForSignature AndHost:host];
        NSString *signature = [self makeSignature:SECRET_ACCESS_ID 
                                             data:[stringForSignature dataUsingEncoding:NSUTF8StringEncoding]];
        
        NSString *urlString = [NSString stringWithFormat:@"http://%@%@?%@&Signature=%@", 
                               host, AWS_PATH, paramsForSignature, signature];
        NSError *error = nil;
        NSXMLDocument *doc = [[[NSXMLDocument alloc] initWithContentsOfURL:[NSURL URLWithString:urlString] 
                                                                   options:0 
                                                                     error:&error] autorelease];    
        
        [self addResultEntriesFromResult:doc ToResultList:resultList];
    }
    
    return resultList;
     
}

- (NSString *) utcTimestamp {
	NSDateFormatter *outputFormatter = [[[NSDateFormatter alloc] init] autorelease];
	outputFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
	outputFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
	return [outputFormatter stringFromDate:[NSDate date]];
}

- (NSMutableString *) queryStringForParameterDictionary:(NSDictionary *) params {

	NSArray *paramNames = [[params allKeys] sortedArrayUsingFunction:stringByteSort context:nil];
	NSMutableString *sigString = [NSMutableString string];
	int i, n = [paramNames count];
	for (i = 0; i < n; i++) {
		NSString *paramName = [paramNames objectAtIndex:i];
		[sigString appendFormat:@"%@=%@", paramName, [[params objectForKey:paramName] stringByEscapingHTTPReserved]];
		if (i < n - 1) [sigString appendString:@"&"];
	}
	return sigString;
}

NSInteger stringByteSort(NSString *a, NSString *b, void *context) {
	return strcmp([a UTF8String], [b UTF8String]);
}

- (NSString *) signatureInputForQueryString:(NSString *) queryString AndHost:(NSString *) host {
	NSMutableString *inputString = [NSMutableString string];
	[inputString appendString:@"GET\n"];
	[inputString appendString:host];
	[inputString appendString:@"\n"];
	[inputString appendString:AWS_PATH];
	[inputString appendString:@"\n"];
	[inputString appendString:queryString];
	return inputString;
}

- (NSString *) makeSignature:(NSString *) salt data:(NSData*) data {

    // This will hold our signature
    unsigned char digest[CC_SHA256_DIGEST_LENGTH];   
    // Get our secret as a raw string
    const char *saltCString = [salt cStringUsingEncoding:NSASCIIStringEncoding];
    // Initialize our HMAC context
    CCHmacContext hctx;
    CCHmacInit(&hctx, kCCHmacAlgSHA256, saltCString, strlen(saltCString));
    // Add in our data
    CCHmacUpdate(&hctx, [data bytes], [data length]);
    // Create our signature
    CCHmacFinal(&hctx, digest);
    // Return a base64 encoded version
    NSData *digestAsString = [NSData dataWithBytes:digest length:CC_SHA256_DIGEST_LENGTH];
    NSString *encodedString = [digestAsString base64Encoding];

    return [encodedString stringByEscapingHTTPReserved];
}

- (int) getTotalPagesFromItemSearchResult:(NSXMLDocument *) xmlResultDoc {

    int totalPages = 0;
    NSError *error;
    NSArray *node = [xmlResultDoc nodesForXPath:@"ItemSearchResponse/Items/TotalPages" error:&error];
    if ([node count] > 0) {
        totalPages = [[[node objectAtIndex:0] stringValue] intValue];
    }
    
    return totalPages;
    
}

- (void) addResultEntriesFromResult:(NSXMLDocument *) xmlResultDoc
                       ToResultList:(NSMutableArray *) resultList {

    NSArray *itemNodes;
    NSError *error;
    int i;
    
    itemNodes = [xmlResultDoc nodesForXPath:@"ItemSearchResponse/Items/Item" error:&error];
    
    for (i=0; i < [itemNodes count]; i++) {    
        [self addDetailsForResultEntryFromItem:[itemNodes objectAtIndex:i] 
                          ToResultList:resultList];
        // Set new status
        NSString *newStatus = [NSString stringWithFormat:@"New entry processed. %i entries processed.", [resultList count]];
        [self sendStatusChange:newStatus];
        
    }
}

- (void) addDetailsForResultEntryFromItem:(NSXMLElement *) xmlElement
                       ToResultList:(NSMutableArray *) resultList {

    NSError *error;
    ResultEntry *resultEntry = [[ResultEntry alloc] init]; 
    
    @try {
                
        // ASIN
        NSArray *node = [xmlElement nodesForXPath:@"ASIN" error:&error];
        if ([node count]) {
            [resultEntry setAsinNumber:[[node objectAtIndex:0] stringValue]];
        }

        // Genre (/ItemSearchResponse/Items/Item/BrowseNodes/BrowseNode[Name="Allgemein"]/Ancestors/BrowseNode/Name)
        node = [xmlElement nodesForXPath:@"BrowseNodes/BrowseNode[Name=\"Allgemein\"]/Ancestors/BrowseNode/Name" 
                                   error:&error];
        if ([node count] > 0) {
            [resultEntry setGenre:[[node objectAtIndex:0] stringValue]];
        }
        else {
            [resultEntry setGenre:@""];
        }
        
        // Label
        node = [xmlElement nodesForXPath:@"ItemAttributes/Label" error:&error];
        if ([node count] > 0) {
            [resultEntry setLabel:[[node objectAtIndex:0] stringValue]];
        }
        else {
            [resultEntry setLabel:@""];            
        }

        // Artist
        node = [xmlElement nodesForXPath:@"ItemAttributes/Artist" error:&error];
        if ([node count] == 1) {
            NSMutableString *albumArtist = [[[node objectAtIndex:0] stringValue] mutableCopy];
            [resultEntry setArtistName:[self replaceSpecialCharacters:albumArtist]];
        }
        else {
            [resultEntry setArtistName:@""];
        }
        
        // Titel
        node = [xmlElement nodesForXPath:@"ItemAttributes/Title" error:&error];
        if ([node count] > 0) {
            NSMutableString *albumName = [[[node objectAtIndex:0] stringValue] mutableCopy];
            [resultEntry setAlbumName:[self replaceSpecialCharacters:albumName]];
        }
        else {
            [resultEntry setAlbumName:@""];
        }
        
        //Release Datum
        node = [xmlElement nodesForXPath:@"ItemAttributes/ReleaseDate" error:&error];
        if ([node count] > 0) {
            [resultEntry setYearOfPublication:[[[node objectAtIndex:0] stringValue] substringToIndex:4]];
        }
        
        // Bild
        node = [xmlElement nodesForXPath:@"LargeImage/URL" error:&error];
        if ([node count] > 0) {
            NSString *path = [[node objectAtIndex:0] stringValue];
            NSURL *url =[NSURL URLWithString:path];
            NSImage *image = [[NSImage alloc] initWithContentsOfURL:url];
            [resultEntry setCover:image];
        }

        // Anzahl der CDs
        node = [xmlElement nodesForXPath:@"ItemAttributes/NumberOfDiscs" error:&error];
        int numberOfDiscs = 0;
        if ([node count] > 0) {
            numberOfDiscs = [[[node objectAtIndex:0] stringValue] intValue];
            [resultEntry setNumberOfDiscs:numberOfDiscs];
        }

        // Lieder
        node = [xmlElement nodesForXPath:@"Tracks/Disc" error:&error];
        if ([node count] > 0) {
            int i, j;
            NSMutableArray *array = [[NSMutableArray alloc] init];
            for (i=0; i < [node count]; i++) {
                NSArray *nodeTracks = [[node objectAtIndex:i] nodesForXPath:@"Track" error:&error];
                for (j=0; j < [nodeTracks count]; j++) {
                    Track *track = [[Track alloc] init];
                    NSMutableString *trackTitle = [[[nodeTracks objectAtIndex:j] stringValue] mutableCopy];
                    track.title = [self replaceSpecialCharacters:trackTitle];
                    track.artist = [resultEntry artistName];
                    [array addObject:track];
                    [track release];
                }
            }
            
            [resultEntry setTrackList:array];
            [resultEntry setNumberOfTracks:[array count]];            
            
        }
    }
    @catch (NSException *exception) {
        NSLog(@"SearchEngine:addDetailsForResultEntryFromItem:ToResultList: Caught %@: %@", 
              [exception name], [exception reason]);
    }
    
    [resultList addObject:resultEntry];
    
}

- (void) sendStatusChange:(NSString *) newStatus {
        
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    NSDictionary *dict = [NSDictionary dictionaryWithObject:newStatus forKey:@"status"];
    [nc postNotificationName:@"STStatusChanged" object:self userInfo:dict];
    
}

- (NSString *) replaceSpecialCharacters:(NSMutableString *) aString {
   
    if ([aString length] > 0) {
        
        // The character : is presented as a slash under Mac OS X, so it should be replaced by another character
        [aString replaceOccurrencesOfString: @":" 
                                 withString: @"-" 
                                    options: 0 
                                      range: NSMakeRange(0, [aString length])];

        
    }

    return aString;
}

- (NSString *) replaceUnWantedCharactersFromSearchString:(NSMutableString *) searchString {

    // The ' character leads to no search results
    [searchString replaceOccurrencesOfString: @"'" 
                                  withString: @"" 
                                     options: 0
                                       range: NSMakeRange(0, [searchString length])];

    return searchString;
    
}

@end
